package org.brownflat.watergate.service;

import org.apache.log4j.Logger;
import org.brownflat.watergate.dao.BranchDAO;
import org.brownflat.watergate.dao.CommitDAO;
import org.brownflat.watergate.dao.CommitElementDAO;
import org.brownflat.watergate.dao.ReviewContributionStateDAO;
import org.brownflat.watergate.dao.ReviewDAO;
import org.brownflat.watergate.dao.ReviewStateDAO;
import org.brownflat.watergate.domain.Branch;
import org.brownflat.watergate.domain.Commit;
import org.brownflat.watergate.domain.CommitElement;
import org.brownflat.watergate.domain.CommitElementAction;
import org.brownflat.watergate.domain.CommitElementType;
import org.brownflat.watergate.domain.Group;
import org.brownflat.watergate.domain.Review;
import org.brownflat.watergate.domain.ReviewContribution;
import org.brownflat.watergate.domain.ReviewContributionState;
import org.brownflat.watergate.domain.ReviewState;
import org.brownflat.watergate.domain.User;
import org.brownflat.watergate.scm.DiffOutput;
import org.brownflat.watergate.scm.EmptyDiffOutput;
import org.brownflat.watergate.scm.RepositoryClient;
import org.brownflat.watergate.scm.RepositoryClientException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import static java.lang.String.format;

/**
 * <p>&copy Rupert Jones 2012</p>
 *
 * @author rup
 */
@Service
@Transactional
public class DefaultReviewService implements ReviewService {

    private static final Logger LOG = Logger.getLogger(DefaultReviewService.class);

    @Autowired
    private ReviewDAO reviewDAO;

    @Autowired
    private ReviewStateDAO reviewStateDAO;

    @Autowired
    private CommitElementDAO commitElementDAO;

    @Autowired
    private BranchDAO branchDAO;

    @Autowired
    private CommitDAO commitDAO;

    @Autowired
    private RepositoryClient repositoryClient;

    @Autowired
    private ReviewContributionStateDAO reviewContributionStateDAO;

    @Override
    public List<Review> findReviewsForCommit(Commit commit) {
        return reviewDAO.findByCommit(commit);
    }

    @Override
    public ReviewState findInitialState() {
        return reviewStateDAO.findInitialState();
    }

    @Override
    public void save(Review review) {
        if(reviewDAO.findById(review.getId()) == null) {
            populateDiffFor(review);
        }
        reviewDAO.upsert(review);
    }

    @Override
    public void generateReviewsFromCommits() {
        try {
            Collection<Branch> branches = branchDAO.findAll();
            for(Branch branch : branches) {
                List<Commit> unscannedCommits = commitDAO.findNotReviewTriggeredFor(branch);
                for(Commit commit : unscannedCommits) {
                    LOG.debug(format("Examining commit [%s] on branch [%s]", commit.getRevision(), branch.getName()));
                    List<Review> reviews = reviewDAO.findByCommit(commit);

                    if(!reviews.isEmpty()) {
                        LOG.debug(format("Commit [%s] already has a review.", commit.getRevision()));
                        recordCommitAsScanned(commit);
                        continue;
                    }

                    if(branch.shouldTriggerReviewWith(commit)) {
                        triggerReview(commit);
                    } else {
                        LOG.debug(format("Trigger rejected commit [%s] for review.", commit.getRevision()));
                    }
                    recordCommitAsScanned(commit);
                }
            }
        } catch (Throwable th) {
            LOG.error(th);
        }
    }

    private void assignContributors(Commit commit, Review review) {
        Branch branch = commit.getBranch();
        Group defaultReviewGroup = branch.getDefaultReviewingGroup();

        if(defaultReviewGroup != null) {
            ReviewContributionState firstState = reviewContributionStateDAO.findInitialState();
            for(User user : defaultReviewGroup.getUsers()) {
                ReviewContribution reviewContribution = new ReviewContribution();
                reviewContribution.setReviewContributionState(firstState);
                reviewContribution.setReview(review);
                reviewContribution.setUser(user);
                LOG.info(format("Adding contributor %s to new review %s", user.getId(), review.getTitle()));
                review.addReviewContribution(reviewContribution);
            }
        }
    }

    private Review triggerReview(Commit commit) {
        Review review = new Review();
        review.addCommit(commit);
        review.setTitle(format("Auto triggered review - %s", commit.getMessage()));
        review.setCreator(commit.getUser());
        review.setReviewState(reviewStateDAO.findInitialState());
        assignContributors(commit, review);
        save(review);
        LOG.info(format("Created review for commit [%s][%s] with [%s] contributions.", commit.getRevision(), commit.getMessage(), review.countContributors()));
        return review;
    }

    private void recordCommitAsScanned(Commit commit) {
        LOG.info(format("Marking commit [%s] as scanned.", commit.getRevision()));
        commit.setReviewTriggerScanned(true);
        commitDAO.upsert(commit);
        LOG.info(format("Marked commit [%s] as scanned.", commit.getRevision()));
    }

    private void populateDiffFor(Review review) {
        for(Commit commit : review.getCommits())  {
            for(CommitElement commitElement : commit.getCommitElements()) {

                if(!commitElement.is(CommitElementType.FILE) || commitElement.hasDiffContent()) {
                    continue;
                }

                String before = null;
                String after = null;
                DiffOutput diffOutput = null;
                try {
                    diffOutput = repositoryClient.diff(commitElement);
                } catch (RepositoryClientException exception) {
                    LOG.warn(format("Couldn't get diff from CommitElement [%s] in commit [%s]", commitElement.getFullPath(), commit.getRevision()));
                    diffOutput = new EmptyDiffOutput();
                }

                if (commitElement.is(CommitElementAction.MODIFY)) {
                    before = diffOutput.getBeforeFile().getOutput();
                    after = diffOutput.getAfterFile().getOutput();
                } else if (commitElement.is(CommitElementAction.ADD)) {
                    after = diffOutput.getAfterFile().getOutput();
                } else if (commitElement.is(CommitElementAction.DELETE)) {
                    before = diffOutput.getBeforeFile().getOutput();
                }
                commitElement.setBeforeContent(before);
                commitElement.setAfterContent(after);
                commitElementDAO.upsert(commitElement);
            }
        }
    }

    public void setReviewDAO(ReviewDAO reviewDAO) {
        this.reviewDAO = reviewDAO;
    }

    public void setCommitElementDAO(CommitElementDAO commitElementDAO) {
        this.commitElementDAO = commitElementDAO;
    }

    public void setRepositoryClient(RepositoryClient repositoryClient) {
        this.repositoryClient = repositoryClient;
    }
}
